package comp6231.project.replica.kelvin;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.nio.ByteBuffer;

import comp6231.project.common.Constants;
import comp6231.project.common.ReplicaState;
import comp6231.project.replica.Replica;

public class ReplicaImpl extends Replica implements RemoteRetailStoreServer {

	private static String UDP_MULTICAST_GROUP = "224.0.0.1";
	private static int UDP_MULTICAST_PORT = 8888;

	private static String UDP_UNICAST_HOSTNAME = "localhost";	

	private StoreLogic storeLogic = new StoreLogic();

	/**
	 * Create a new instance
	 * @param replicaID ID can be Constants.REPLICA_ID_M2, Constants.REPLICA_ID_T2 or Constants.REPLICA_ID_V2
	 * @throws Exception
	 */
	public ReplicaImpl(String replicaID) throws Exception {
		System.out.println("Creating replica " + replicaID);
		setReplicaId(replicaID);
		storeLogic.init(getItemsList(), getStorageDir(), this);
		
		(new CheckStockThread()).start();
		(new BuyThread()).start();
	}
	
	private String getItemsList() {
		if (getReplicaId().equals(Constants.REPLICA_ID_M2)) {
			return "./drs2data/montreal-retail-store-stock.txt";
		} else if (getReplicaId().equals(Constants.REPLICA_ID_T2)) {
			return "./drs2data/toronto-retail-store-stock.txt";
		} else {
			return "./drs2data/vancouver-retail-store-stock.txt";
		}		
	}
	
	private String getStorageDir() {
		if (getReplicaId().equals(Constants.REPLICA_ID_M2)) {
			return "./drs2data/m";
		} else if (getReplicaId().equals(Constants.REPLICA_ID_T2)) {
			return "./drs2data/t";
		} else {
			return "./drs2data/v";
		}		
	}
	
	private int getUnicastPort() {
		return getUnicastPort(getReplicaId());
	}
	
	private int getUnicastPort(String id) {
		if (id.equals(Constants.REPLICA_ID_M2)) {
			return 10500;
		} else if (id.equals(Constants.REPLICA_ID_T2)) {
			return 10600;
		} else {
			return 10700;
		}		
	}
	
	public String[] getOtherStoreNames() {
		if (getReplicaId().equals(Constants.REPLICA_ID_M2)) {
			return new String[] { Constants.REPLICA_ID_T2, Constants.REPLICA_ID_V2 };
		} else if (getReplicaId().equals(Constants.REPLICA_ID_T2)) {
			return new String[] { Constants.REPLICA_ID_M2, Constants.REPLICA_ID_V2 };
		} else {
			return new String[] { Constants.REPLICA_ID_T2, Constants.REPLICA_ID_M2 };
		}
	}

	// Background thread to handle Buy request from another server
	class BuyThread extends Thread {

		public BuyThread() {
			super("BuyThread");
		}

		public void run() {
			DatagramSocket dsIn = null;
			DatagramSocket dsOut = null;
			try {
				dsIn = new DatagramSocket(getUnicastPort());
				while (true) {
					try {
						final byte bufIn[] = new byte[256];
						final DatagramPacket pack = new DatagramPacket(bufIn,
								bufIn.length);
						dsIn.receive(pack);

						ByteBuffer bb = ByteBuffer.wrap(bufIn);
						final String customerID = Utils.getString(bb);
						final String itemID = Utils.getString(bb);
						final int numberOfItem = bb.getInt();
						System.out.println(
								"Received buy request: customerID="
										+ customerID + ", itemID=" + itemID);

						// Spawn a new thread to process the request
						Runnable r = new Runnable() {
							public void run() {
								try {
									int result = storeLogic
											.buyOpInternal(customerID, itemID,
													numberOfItem);

									byte bufOut[] = new byte[256];
									ByteBuffer bbOut = ByteBuffer.wrap(bufOut);
									bbOut.putInt(result);

									System.out.println(
											"Sending result to "
													+ pack.getAddress()
													+ ", port "
													+ pack.getPort()
													+ ": result=" + result);
									DatagramPacket packOut = new DatagramPacket(
											bufOut, 4, pack.getAddress(),
											pack.getPort());
									DatagramSocket dsOut = new DatagramSocket();
									dsOut.send(packOut);
								} catch (Exception ex) {
									System.out.println("buy failed" + ex);
								}
							}
						};
						(new Thread(r)).start();

					} catch (Exception ex) {
						System.out.println("buy failed" + ex);
					}
				}
			} catch (Exception ex) {
				System.out.println("BuyThread failed" + ex);

			} finally {
				if (dsIn != null) {
					dsIn.close();
				}
				if (dsOut != null) {
					dsOut.close();
				}
			}
		}

	}

	// Background thread to handle CheckStock request from another server
	class CheckStockThread extends Thread {

		public CheckStockThread() {
			super("CheckStockThread");
		}

		public void run() {
			System.out.println("Started.");

			MulticastSocket msIn = null;
			InetAddress group = null;
			DatagramSocket dsOut = null;
			try {
				String groupName = ReplicaImpl.UDP_MULTICAST_GROUP;
				group = InetAddress.getByName(groupName);
				msIn = new MulticastSocket(ReplicaImpl.UDP_MULTICAST_PORT);
				msIn.joinGroup(group);

				dsOut = new DatagramSocket();

				while (true) {
					try {
						byte bufIn[] = new byte[256];
						DatagramPacket pack = new DatagramPacket(bufIn,
								bufIn.length);
						msIn.receive(pack);

						ByteBuffer bb = ByteBuffer.wrap(bufIn);
						String srcServer = Utils.getString(bb);
						if (!srcServer.equals(getReplicaId())) {
							String itemID = Utils.getString(bb);
							System.out.println(
									"Received through multicast: itemID="
											+ itemID);

							int numberOfItem = storeLogic
									.checkStockOpInternal(itemID);

							byte bufOut[] = new byte[256];
							bb = ByteBuffer.wrap(bufOut);
							bb.putInt(numberOfItem);
							System.out.println(
									"Sending to " + pack.getAddress()
											+ ", port " + pack.getPort()
											+ ": numberOfItem=" + numberOfItem);
							pack = new DatagramPacket(bufOut, 4, pack
									.getAddress(), pack.getPort());
							dsOut.send(pack);
						}

					} catch (Exception ex) {
						System.out.println("checkStock failed" + ex);
					}
				}
			} catch (Exception ex) {
				System.out.println("CheckStockThread failed" + ex);

			} finally {
				if (msIn != null) {
					if (group != null) {
						try {
							msIn.leaveGroup(group);
						} catch (IOException ex) {
						}
					}
					msIn.close();
				}
				if (dsOut != null) {
					dsOut.close();
				}
			}
		}
	}

	public int buyFromServer(String serverName, String customerID,
			String itemID, int numberOfItem) {
		try {
			DatagramSocket ds = new DatagramSocket();

			int count = 0;
			byte bufOut[] = new byte[256];
			ByteBuffer bb = ByteBuffer.wrap(bufOut);
			count += Utils.putString(bb, customerID);
			count += Utils.putString(bb, itemID);
			bb.putInt(numberOfItem);
			count += 4;

			DatagramPacket pack = new DatagramPacket(bufOut, count, InetAddress
					.getByName(UDP_UNICAST_HOSTNAME), getUnicastPort(serverName));
			ds.send(pack);

			byte bufIn[] = new byte[256];
			pack = new DatagramPacket(bufIn, bufIn.length);
			ds.receive(pack);
			ds.close();

			bb = ByteBuffer.wrap(bufIn);
			int result = bb.getInt();
			return result;

		} catch (Exception ex) {

		}
		return 0;
	}

	public int checkStockFromOtherServers(String itemID) {
		int result = 0;
		String[] otherServers = getOtherStoreNames();
		if (otherServers != null && otherServers.length > 0) {
			DatagramSocket ds = null;
			try {
				String groupName = ReplicaImpl.UDP_MULTICAST_GROUP;
				InetAddress group = InetAddress.getByName(groupName);
				int port = ReplicaImpl.UDP_MULTICAST_PORT;

				byte bufOut[] = new byte[256];
				ByteBuffer bb = ByteBuffer.wrap(bufOut);
				Utils.putString(bb, getReplicaId());
				Utils.putString(bb, itemID);
				ds = new DatagramSocket();
				DatagramPacket pack = new DatagramPacket(bufOut, bufOut.length,
						group, port);
				System.out.println(
								"Sending through multicast: " + itemID
										+ " addr=" + pack.getAddress()
										+ " port=" + pack.getPort());
				ds.send(pack);

				for (int i = 0; i < otherServers.length; i++) {
					byte bufIn[] = new byte[256];
					pack = new DatagramPacket(bufIn, bufIn.length, pack
							.getAddress(), pack.getPort());
					ds.receive(pack);
					bb = ByteBuffer.wrap(bufIn);
					int numItems = bb.getInt();
					result += numItems;
					System.out.println("Received: " + numItems);
				}

				ds.close();

			} catch (Exception ex) {
				ex.printStackTrace();

			} finally {
				if (ds != null) {
					ds.close();
				}
			}
		}
		return result;
	}

	@Override
	public int buyOp(String customerID, String itemID, int numberOfItem) {
		return storeLogic.buyOp(customerID, itemID, numberOfItem);
	}

	@Override
	public int returnOp(String customerID, String itemID, int numberOfItem) {
		return storeLogic.returnOp(customerID, itemID, numberOfItem);
	}

	@Override
	public int checkStockOp(String itemID) {
		return storeLogic.checkStockOp(itemID);
	}

	@Override
	public int exchangeOp(String customerID, String boughtItemID,
			int boughtNumber, String desiredItemID, int desiredNumber) {
		return storeLogic.exchangeOp(customerID, boughtItemID, boughtNumber, desiredItemID, desiredNumber);
	}

	@Override
	public ReplicaState getReplicaState() {
		ReplicaState state = new ReplicaState();
		storeLogic.transferToStoreState(state);
		return state;
	}
	
	@Override
	public void setReplicaState(ReplicaState state) {
		storeLogic.transferFromStoreState(state);
	}
	
	public static void main(String[] args) {
		try {
			Replica m2 = new ReplicaImpl(Constants.REPLICA_ID_M2);
			
			ReplicaState ss = m2.getReplicaState();
			Replica ttt = new ReplicaImpl(Constants.REPLICA_ID_T2);	
			ttt.setReplicaState(ss);
			
			byte[] buf = comp6231.project.common.Utils.toBytes(ss);
			
			Replica t2 = new ReplicaImpl(Constants.REPLICA_ID_T2);		

			Replica v2 = new ReplicaImpl(Constants.REPLICA_ID_V2);		

			m2.start();
			t2.start();
			v2.start();
			m2.join();
			t2.join();
			v2.join();
		} catch (Exception ex) {
			ex.printStackTrace();
		}		
	}
}
